﻿#region License

// // -------------------------------------------------------------------------------------
// // This information refers to EventFetchingService.cs
// // 
// // Copyright 2012 ELEN7046 Group 2
// // 
// // Last Modified on: 2012-06-10
// // 
// // Contributors:
// // 
// // Amith Sewnarain
// // Elmar Vosloo
// // Jason Thorne
// // Satish Jewan
// // 
// // Wits University 
// // School of Electrical & Information Engineering
// // Software Engineering Tools and Techniques
// // 
// // -------------------------------------------------------------------------------------
// // 
// // This file (EventFetchingService.cs) is part of the DishDash Event Visualization System.
// // 
// // DishDash is free software: you can redistribute it and/or modify
// // it under the terms of the GNU General Public License as published by
// // the Free Software Foundation, either version 3 of the License, or
// // (at your option) any later version.
// // 
// // DishDash is distributed in the hope that it will be useful,
// // but WITHOUT ANY WARRANTY; without even the implied warranty of
// // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// // GNU General Public License for more details.
// // 
// // You should have received a copy of the GNU General Public License
// // along with DishDash.  
// // If not, see <http://www.gnu.org/licenses/>.
// // -------------------------------------------------------------------------------------

#endregion

#region Using Directives

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Custom.Web.Extensions;
using DishDash.Handlers.Messages.EventMessages;
using MongoDB.Bson;
using MongoDB.Driver;
using MongoDB.Driver.Builders;

#endregion

namespace DishDash.Handlers
{
    /// <summary>
    ///   JSON/HTTP Service that deals with querying of event related data.
    /// </summary>
    public class EventFetchingService : MethodRoutingJsonHandler
    {
        // MongoDB server and database
        private readonly MongoDatabase _database;
        private readonly MongoServer _server;

        /// <summary>
        ///   Initializes a new instance of the <see cref="EventFetchingService" /> class.
        /// </summary>
        public EventFetchingService()
        {
#warning Move the log database name to a configuration file.
            _server = MongoServer.Create();
            _database = _server.GetDatabase("LogStore");
        }

        /// <summary>
        ///   Gets the all event data specified criteria.
        /// </summary>
        /// <param name="request"> The request. </param>
        /// <returns> </returns>
        public CategoryDataResponse GetCategoryData(CategoryDataRequest request)
        {
            // Set collection which will be used during execution
            var collection = this.GetCollection("PerformanceEvents");


            // Loop over items in request and build a query document
            var query = new QueryDocument();

            foreach (var category in request.Categories)
            {
                query.Add("Category", BsonRegularExpression.Create(category, "i"));
            }

            // Build query
            var result =
                collection
                    .FindAs<PerformanceEvent>(query)
                    .SetSortOrder(SortBy.Descending("TimeStamp"));

            var response = new CategoryDataResponse {PerformanceEvents = new List<PerformanceEvent>()};

            //Loop over each item in the query result set and build up the response which will be returned.
            foreach (var item in result)
            {
                var performanceEvent = new PerformanceEvent {ContextValues = new Dictionary<string, string>()};
                performanceEvent.Category = item.Category;
                performanceEvent.ContextValues = item.ContextValues;
                performanceEvent.TimeStamp = item.TimeStamp;
                performanceEvent.Value = item.Value;

                response.PerformanceEvents.Add(item);
            }

            return response;
        }

        /// <summary>
        ///   Gets the aggregated count of an event by event type.
        /// </summary>
        /// <param name="request"> The request. </param>
        /// <returns> </returns>
        public AggregatedEventCountByEventTypeResponse GetAggregatedEventCountByEventType(
            GetAggregatedEventCountByEventTypeRequest request)
        {
            // Get the start time from the request in ticks
            var startTimeInTicks = new TimeSpan(DateTime.Now.Ticks - (DateTime.Now.Ticks - request.StartTime.Ticks));

            // Set the collection which will be used during processing
            var collectionName = "PerformanceEventsOneSecondPrecision";

            // Get the collection from the store
            var collection = this.GetCollection(collectionName);

            //The id of the record in mongo is a timestamp. Configure query to search for events where the record was created after the specified start time.
            var query = Query.GTE("_id",
                                  Convert.ToInt64(startTimeInTicks.TotalSeconds).ToString(CultureInfo.InvariantCulture));

            // Create a list which will hold a list of the all aggregated events (There may be multiple items in this list due to multiple event types being queried)
            var biggerEventCountAggregateList = new List<EventCountAggregate>();

            // Loop over all the event types specified by the caller.
            foreach (var eventType in request.EventTypes)
            {
                //Execute the query configured above over the mongo document collection. Note that the .SetFields(...) is used to bring back those records which contain the event type specified.
                var result =
                    collection.Find(query)
                        .SetFields(eventType)
                        .SetSortOrder(SortBy.Ascending("_id"));

                // Create a list of aggregated events which will contain only the items related to a particular event type.
                var eventCountAggregateList = new List<EventCountAggregate>();

                // Loop over each item which was returned by the query
                foreach (var item in result)
                {
                    long sum;
                    int count;

                    // Invoke algorithm to grab the sum and count from the item in the result.
                    GetSumAndCount(eventType, item, out sum, out count);

                    // Add the aggregated item to the minor list.
                    eventCountAggregateList.Add(new EventCountAggregate
                                                    {EventType = eventType, AggregatedCount = count, Sum = sum});
                }

                // Summarize all the events in the minor list.
                var eventSummary = from b in eventCountAggregateList
                                   group b by b.EventType
                                   into g
                                   let nrEvents = g.Count()
                                   let totalSum = g.Sum(w => w.Sum)
                                   let totalCount = g.Sum(v => v.AggregatedCount)
                                   select new EventCountAggregate
                                              {
                                                  EventType = g.Key,
                                                  Sum = totalSum,
                                                  AggregatedCount = totalCount
                                              };

                // Add all the events which were summarized to the a result list
                biggerEventCountAggregateList.AddRange(eventSummary);
            }

            // Add all result lists to the response which will be returned to the caller.
            return new AggregatedEventCountByEventTypeResponse
                       {AggregatedEventCounts = biggerEventCountAggregateList.ToArray<EventCountAggregate>()};
        }


        /// <summary>
        ///   Gets events based on a specific precision.
        /// </summary>
        /// <param name="request"> The request. </param>
        /// <returns> </returns>
        public CategoryDataByPrecisionResponse GetCategoryDataByPrecision(CategoryDataByPrecisionRequest request)
        {
            // Get the start time in number of ticks since 01/01/0001
            var ticks = new TimeSpan(DateTime.Now.Ticks - (DateTime.Now.Ticks - request.StartTime.Ticks));

            #region Determine database and starting documentID

            // Because different precisions of time are stored in different collections we need
            //  to determine which document collection to use.
            // In the case of each collection we also need to determine which document id to start returning 
            //  data from. Document ids are number of seconds/minutes/hours/days since 01/01/0001
            string collectionName;
            long documentId;

            switch (request.Precision.ToUpperInvariant())
            {
                case "SECOND":
                    collectionName = "PerformanceEventsOneSecondPrecision";
                    documentId = Convert.ToInt64(ticks.TotalSeconds);
                    break;
                case "MINUTE":
                    collectionName = "PerformanceEventsOneMinutePrecision";
                    documentId = Convert.ToInt64(ticks.TotalMinutes);
                    break;
                case "HOUR":
                    collectionName = "PerformanceEventsOneHourPrecision";
                    documentId = Convert.ToInt64(ticks.TotalHours);
                    break;
                case "DAY":
                    collectionName = "PerformanceEventsOneDayPrecision";
                    documentId = Convert.ToInt64(ticks.TotalDays);
                    break;
                default:
                    throw new ArgumentException(String.Format("The specified '{0}' precision is not supported.",
                                                              request.Precision));
            }

            #endregion

            // Get the collection based on the determined name
            var collection = this.GetCollection(collectionName);

            // Setup query from the start document id
            var query = Query.GT("_id", documentId.ToString(CultureInfo.InvariantCulture));

            var response = new CategoryDataByPrecisionResponse {Categories = new List<Category>()};

#warning should parallelize this or find a way of using the group by function to execute as one request on the database
            // Loop over each requested event type and add to the response after padding the data to account for 
            // seconds/minutes/hours/days of inactivity
            foreach (var category in request.Categories)
            {
                // Get the results of the query
                var result =
                    collection
                        .Find(query).SetFields(category)
                        .SetSortOrder(SortBy.Ascending("_id"));

                // Setup the response item for this event
                var resultCategory = new Category
                                         {CategoryName = category, PrecisionDocuments = new List<PrecisionDocument>()};

                long previousDocumentId = -1;

                // Trasform the result and pad
                foreach (var item in result)
                {
                    long sum;
                    int count;
                    GetSumAndCount(category, item, out sum, out count);

                    var id = item.GetElement("_id").Value.ToInt64();

                    #region Pad the collection

                    if (previousDocumentId > -1)
                    {
                        if (previousDocumentId + 1 != id)
                        {
                            for (long i = previousDocumentId + 1; i < id; i++)
                            {
                                resultCategory.PrecisionDocuments.Add(
                                    new PrecisionDocument
                                        {
                                            TimeStamp = i,
                                            Count = 0,
                                            Sum = 0
                                        });
                            }
                        }
                    }

                    #endregion

                    var precisionDocument = new PrecisionDocument {TimeStamp = id, Sum = sum, Count = count};

                    resultCategory.PrecisionDocuments.Add(precisionDocument);

                    previousDocumentId = id;
                }

                // Add the event's details to the response
                response.Categories.Add(resultCategory);
            }

            return response;
        }

        /// <summary>
        ///   Groups all instances of the specified events that are in a specific sample period for the value of a context key. Useful for scenarios where applications are clustered and you would like to view which events occurred on a specific application server.
        /// </summary>
        /// <param name="request"> The request. </param>
        /// <returns> </returns>
        public AggregateEventCountByContextValueResponse AggregateEventCountByContextValue(
            AggregateEventCountByContextValueRequest request)
        {
            // Use the PerformanceEvents collection as it is the only one that stores the context values
            const string collection = "PerformanceEvents";

            // Determine number of ticks since the specified start time.
            var ticks = new TimeSpan(DateTime.Now.Ticks - (DateTime.Now.Ticks - request.StartTime.Ticks));
            long timeStamp;

            // Determine precision in which the start time falls and the key in to use when grouping
            //  the events
            string keyName;
            switch (request.Precision.ToUpperInvariant())
            {
                case "SECOND":
                    timeStamp = Convert.ToInt64(ticks.TotalSeconds);
                    keyName = "Precisions.Second";
                    break;
                case "MINUTE":
                    timeStamp = Convert.ToInt64(ticks.TotalMinutes);
                    keyName = "Precisions.Minute";
                    break;
                case "HOUR":
                    timeStamp = Convert.ToInt64(ticks.TotalHours);
                    keyName = "Precisions.Hour";
                    break;
                case "DAY":
                    timeStamp = Convert.ToInt64(ticks.TotalDays);
                    keyName = "Precisions.Day";
                    break;
                default:
                    throw new ArgumentException(String.Format("The specified '{0}' precision is not supported.",
                                                              request.Precision));
            }

            // Get the collection and setup the response
            var events = GetCollection(collection);
            var response = new AggregateEventCountByContextValueResponse();

            // keep track of the minimum and maximum values so that we can pad the response for each event uniformly
            long max = 0;
            long min = long.MaxValue;

            // for a background in how mongoDB group queries an result reducing works refer to:
            //  http://kylebanker.com/blog/2009/11/mongodb-count-group/

            #region loop over each event name and perform the query and transform the result to the response format

            for (var i = 0; i < request.EventNames.Count; i++)
            {
                // Setup the query using the event name, the context key and value to filter by and the
                //  precision key name and value               
                var query = Query.And(
                    Query.EQ("Category", request.EventNames[i]),
                    Query.EQ("ContextValues." + request.ContextKey, request.ContextValue),
                    Query.GTE(keyName, timeStamp));

                // Setup the initial document 
                var initial = new BsonDocument("sum", 0);

                // Create groupby key, this seems to work better than just specifying the string format i.e {'key' : true}
                var groupyBy = GroupBy.Keys(keyName);

                // Perform the query reducing the unique result sets using the specified javascript function
                var groupQuery = events.Group(query, groupyBy, initial, "function(doc,prev){ prev.sum += 1}", "");

                // Transform the result
                var eventCounts = from g in groupQuery
                                  select new EventCount
                                             {
                                                 TimeStamp = g[keyName].AsInt64,
                                                 Count = Convert.ToInt64(g["sum"].AsDouble)
                                             };

                // Add the results for this event to the response
                response
                    .EventCounts
                    .Add(new Event
                             {
                                 EventName = request.EventNames[i],
                                 EventCounts = eventCounts.ToList()
                             });


                // determine the new maximum timestamp
                if (!eventCounts.Any())
                {
                    continue;
                }
                var maxTime = (from e in eventCounts
                               select e.TimeStamp).Max();
                if (maxTime > max)
                {
                    max = maxTime;
                }

                // determine the new maximum timestamp
                var minTime = (from e in eventCounts
                               select e.TimeStamp).Min();
                if (minTime < min)
                {
                    min = minTime;
                }
            }

            #endregion

            // Pad the responses all the events using the maximum and minimum values so that a uniform number of entries are
            //  return per event type.

            // Only loop for as many events as there are
            for (int i = 0; i < response.EventCounts.Count; i++)
            {
                // loop from minimum value to the maximum value filling in any missing data points
                for (long j = min; j < max + 1; j++)
                {
                    if (response.EventCounts[i].EventCounts.Contains(new EventCount {TimeStamp = j},
                                                                     new EventCountEqualityComparer()))
                    {
                        continue;
                    }

                    response.EventCounts[i].EventCounts.Insert(0, new EventCount {TimeStamp = j, Count = 0});
                }

                // sort the resulting collection
                response.EventCounts[i].EventCounts.Sort(
                    (x, y) =>
                        {
                            if (x.TimeStamp > y.TimeStamp)
                            {
                                return 1;
                            }
                            else if (x.TimeStamp < y.TimeStamp)
                            {
                                return -1;
                            }
                            else
                            {
                                return 0;
                            }
                        });
            }

            return response;
        }

        /// <summary>
        ///   Gets the sum and count of the specified document. (Utility method)
        /// </summary>
        /// <param name="category"> The category. </param>
        /// <param name="document"> The document. </param>
        /// <param name="sum"> The sum. </param>
        /// <param name="count"> The count. </param>
        private static void GetSumAndCount(string category, BsonDocument document, out long sum, out int count)
        {
            var parts = category.Split('.');

            BsonDocument child = document[parts[0]].AsBsonDocument;
            for (int i = 1; i < parts.Length; i++)
            {
                if (child.Contains(parts[i]))
                {
                    child = child[parts[i]].AsBsonDocument;
                }
            }

            long tempsum = 0;
            int tempcount = 0;

            if (child.Contains("sum"))
            {
                tempsum = child["sum"].AsInt64;
            }

            if (child.Contains("count"))
            {
                tempcount = child["count"].AsInt32;
            }

            sum = tempsum;
            count = tempcount;
        }

        /// <summary>
        ///   Gets the events which can be associated with the input criteria.
        /// </summary>
        /// <param name="request"> The request. </param>
        /// <returns> </returns>
        public GetCategoriesResponse GetCategories(GetCategoriesRequest request)
        {
            // Set the collection to use during execution
            var collection = this.GetCollection("PerformanceEvents");

            // Configure query. All events with an id > than 0 will be returned
            var query = Query.EQ("_id", "0");

            // Find the events
            var result = collection.Find(query);

            //Build a response to return to the caller
            var response = new GetCategoriesResponse {Categories = new List<string>()};

            if (result.Any())
            {
                var categories = result.ElementAt(0)["categoryValues"].AsBsonArray;

                foreach (var category in categories)
                {
                    response.Categories.Add(category.AsString);
                }
            }

            return response;
        }

        /// <summary>
        ///   Gets the mongo collection specified.
        /// </summary>
        /// <param name="collectionName"> Name of the collection. </param>
        /// <returns> </returns>
        private MongoCollection<BsonDocument> GetCollection(string collectionName)
        {
            return _database.GetCollection(collectionName);
        }
    }
}